/**
 * Copyright (c) 2009  Collaborative Development Group, C.S. Dept., University of Bari
 *
 * All rights reserved. This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License v1.0  which accompanies this distribution, and is 
 * available at http://www.eclipse.org/legal/epl-v10.html
 */
package it.uniba.di.cdg.penelope.events;

import static org.osgi.service.event.EventConstants.*;
import static it.uniba.di.cdg.penelope.events.IEventHelper.APP_EVENT;
import static it.uniba.di.cdg.penelope.util.ReflectionUtilities.*;
import static it.uniba.di.cdg.penelope.events.Utilities.getTopicId;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.List;
import java.util.Properties;

import it.uniba.di.cdg.penelope.AbstractDependencyInjectionAspect;
import it.uniba.di.cdg.penelope.annotation.Injectable;
import it.uniba.di.cdg.penelope.annotation.Start;
import it.uniba.di.cdg.penelope.annotation.Stop;
import it.uniba.di.cdg.penelope.events.ApplicationEventListener;
import it.uniba.di.cdg.penelope.events.EventHandlerInvocationException;
import it.uniba.di.cdg.penelope.events.OnEvent;

import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;

import com.google.inject.Inject;

/**
 * Augments objects that want to participate to the Application Event Bus. For each candidate listener, an 
 * aspect instance is created: note that this aspect only works if an {@link AbstractDependencyInjectionAspect}
 * instance is present because it needs the {@link BundleContext} being injected in order to publishing to 
 * work.
 * <br/>
 * Derived aspects must only implement the {@link ApplicationEventEnhancerAspect#scope()} pointcut to define
 * the applicability scope for this aspect.
 */
@Injectable
public abstract aspect AbstractApplicationEventEnhancerAspect perthis( eventListenerObject() ) {
	// Trick for publishing EventHandler to the weaver
	public interface IEventListener extends EventHandler {}
	
	/**
	 * Left to derived aspects to define application scope for this aspect.
	 */
	public pointcut scope();
	
	@Inject
	private BundleContext bundleContext = null;
	
	private String filter = "";
	
	private ServiceRegistration handlerRegistration;

	/**
	 * Our implementation simply unwrap the application event payload from the EventAdmin {@link Event} object
	 * and dispatches it to the right method through reflection. 
	 * @param event
	 */
	public void IEventListener.handleEvent(Event event) {
		Object applicationEvent = getApplicationEvent( event );
		if (applicationEvent == null)
			return;
		
		Method[] eventHandlers = getAnnotatedMethods( this.getClass(), OnEvent.class );
		for (Method method : eventHandlers) {
			final Class<?> targetEventClass = method.getParameterTypes()[0];
			
			if (applicationEvent.getClass().equals( targetEventClass )) {
				try {
					method.invoke( this, applicationEvent );
				} catch (Throwable e) {
					throw new EventHandlerInvocationException( e );
				}
				// XXX Only one invocation: multiple event handlers are not supported!
				return;
			}
		}
	}
	
	/**
	 * Every object annotated with @ApplicationEventListener should implement {@link EventHandler} interface
	 * in order to be published to the OSGi AdminService.
	 */
	declare parents: (@ApplicationEventListener *) implements IEventListener;

	/**
	 * Only apply to classes annotated with @ApplicationEventListener and within scope. 
	 */
	pointcut eventListenerObject() : scope() && @this(ApplicationEventListener);

	/**
	 * Captures object creation.
	 * @param listener
	 * @param annotation
	 */
	pointcut listenerCreation( IEventListener listener, ApplicationEventListener annotation ) : 
//		!within(AbstractApplicationEventEnhancerAspect+) 
//		&& 
		initialization( public @ApplicationEventListener *.new(..) ) 
		&& this( listener ) && @this( annotation );
	
	pointcut onListenerStart( IEventListener listener ) :
		@within( ApplicationEventListener )
		&& execution( @Start * *.*(..) ) && this( listener );
	
	pointcut onListenerStop( IEventListener listener ) :
		@within( ApplicationEventListener )
		&& execution( @Stop * *.*(..) ) && this( listener );
	
	/**
	 * When object is created, grab the filter put into its annotation.
	 * @param listener
	 * @param annotation
	 */
	after( IEventListener listener, ApplicationEventListener annotation ) : listenerCreation( listener, annotation ) {
		this.filter = annotation.value();
	}
	
	/**
	 * When the listener is started, by invoking any method or costructor annotated with @Start, 
	 * publish it to the event admin.
	 * @param listener
	 */
	after (IEventListener listener) : onListenerStart( listener ) {
		// find the listener methods
		Method[] eventHandlers = getAnnotatedMethods( listener.getClass(), OnEvent.class );
		// collect the topics
		List<String> topics = new ArrayList<String>();
		for (Method method : eventHandlers) {
			if (method.getParameterTypes().length != 1) {
				throw new EventHandlerInvocationException( "Only a method with one (and only one) event parameter can be annotated with @OnEvent!" );
			}
			topics.add( getTopicId( method.getParameterTypes()[0] ) );
		}
		// register this listener using the bundle context
		Dictionary<Object,Object> properties = new Properties();
		properties.put( EVENT_TOPIC, topics );
		if (!filter.isEmpty()) {
			properties.put( EVENT_FILTER, filter );
		}
		handlerRegistration = bundleContext.registerService( EventHandler.class.getName(), listener, properties );
	}

	/**
	 * When the method annotated with the @Stop annotation is invoked, the listener is removed from the event bus.
	 * @param listener
	 */
	after (IEventListener listener) : onListenerStop( listener ) {
		// unregister the listener
		if (handlerRegistration != null) {
			handlerRegistration.unregister();
			handlerRegistration = null;
		}
	}

	/**
	 * @param event
	 * @return
	 */
	private static Object getApplicationEvent( Event event ) {
		return event.getProperty( APP_EVENT );
	}
}